// ---------------------------------------------------------------------------
// Campari Software
//
// FileDownloader.cs
//
// Provides the implementation of a FileDownloader. This class encapsulates
// all of the communication required to download a file from a URI.
//
// ---------------------------------------------------------------------------
// Copyright (C) 2006-2007 Campari Software
// All rights reserved.
//
// THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY
// OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT
// LIMITED TO THE IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR
// FITNESS FOR A PARTICULAR PURPOSE.
// ---------------------------------------------------------------------------
using System;
using System.ComponentModel;
using System.Globalization;
using System.IO;
using System.Net;
using System.Runtime.InteropServices;
using System.Threading;

namespace Nojin.Netwwork.Download
{
    /// <summary>
    /// Download a file from a URI.
    /// </summary>
    public class FileDownloader : Component, IDisposable
    {
        #region events

        /// <summary>
        /// Represents the method that will handle the <see cref="FileDownloader.DownloadCompleted"/> event of a <see cref="FileDownloader"/> download request.
        /// </summary>
        /// <remarks>When you create a <see cref="System.EventHandler"/> delegate, you identify the method that 
        /// will handle the event. To associate the event with your event handler, add an instance of the delegate
        /// to the event. The event handler is called whenever the event occurs, unless you remove the delegate.
        /// For more information about event handler delegates, see Events and Delegates.</remarks>
        public event EventHandler<FileDownloadCompletedEventArgs> DownloadCompleted;

        /// <summary>
        /// Represents the method that will handle the <see cref="FileDownloader.DownloadProgressChanged"/> event of a <see cref="FileDownloader"/> download request.
        /// </summary>
        /// <remarks>When you create a <see cref="System.EventHandler"/> delegate, you identify the method that 
        /// will handle the event. To associate the event with your event handler, add an instance of the delegate
        /// to the event. The event handler is called whenever the event occurs, unless you remove the delegate.
        /// For more information about event handler delegates, see Events and Delegates.</remarks>
        public event EventHandler<FileDownloadProgressChangedEventArgs> DownloadProgressChanged;

        /// <summary>
        /// Represents the method that will handle the <see cref="FileDownloader.DownloadStatusChanged"/> event of a <see cref="FileDownloader"/> download request.
        /// </summary>
        /// <remarks>When you create a <see cref="System.EventHandler"/> delegate, you identify the method that 
        /// will handle the event. To associate the event with your event handler, add an instance of the delegate
        /// to the event. The event handler is called whenever the event occurs, unless you remove the delegate.
        /// For more information about event handler delegates, see Events and Delegates.</remarks>
        public event EventHandler<FileDownloadStatusChangedEventArgs> DownloadStatusChanged;

        #endregion

        #region Fields
        private const int DefaultBlockSize = 8096;

        private BackgroundWorker backgroundWorker;
        private int blockSize = FileDownloader.DefaultBlockSize;
        private ICredentials credentials;
        private bool disposed;
        private bool downloadHtml;
        private string downloadingTo;
        private bool overwrite;
        private bool preAuthenticate;
        private IWebProxy proxy;
        private AutoResetEvent syncEvent;
        private TimeSpan timeout = FileDownloader.IndefiniteTimeout;
        private string userAgent = null;
        #endregion

        #region Constructor
        /// <summary>
        /// Initializes a new instance of the <see cref="FileDownloader"/> class.
        /// </summary>
        public FileDownloader()
        {
            // Set the default web proxy
            this.proxy = WebRequest.DefaultWebProxy;

            // Initialize the sync event for synchronous downloads
            this.syncEvent = new AutoResetEvent(true);

            // Initialize the background worker for async downloads
            this.backgroundWorker = new BackgroundWorker();            
            this.backgroundWorker.WorkerReportsProgress = true;
            this.backgroundWorker.WorkerSupportsCancellation = true;
            this.backgroundWorker.DoWork += new System.ComponentModel.DoWorkEventHandler(this.DownloadAsyncCore);
            this.backgroundWorker.RunWorkerCompleted += new System.ComponentModel.RunWorkerCompletedEventHandler(this.DownloadAsyncCompleted);
            this.backgroundWorker.ProgressChanged += new System.ComponentModel.ProgressChangedEventHandler(this.DownloadAsyncProgressChanged);
        }
        #endregion


        #region Private Methods

        #region CheckDisposed
        /// <summary>
        /// Protects against the <see cref="FileDownloader"/> from being used
        /// after it has been Disposed.
        /// </summary>
        private void CheckDisposed()
        {
            if (this.disposed)
            {
                throw new ObjectDisposedException(base.GetType().FullName);
            }
        }
        #endregion

        #region DownloadAsyncCompleted
        /// <summary>
        /// Raises the <see cref="DownloadCompleted"/> event by responding to
        /// the <see cref="BackgroundWorker.RunWorkerCompleted"/> event of 
        /// the background worker.
        /// </summary>
        /// <param name="sender">The background worker that completed.</param>
        /// <param name="e">The completion data.</param>
        private void DownloadAsyncCompleted(object sender, RunWorkerCompletedEventArgs e)
        {
            // Signal that the work has been completed
            this.syncEvent.Set();

            this.OnDownloadCompleted(e);
        }
        #endregion

        #region DownloadAsyncCore
        /// <summary>
        /// Downloads the file by responding to the 
        /// <see cref="BackgroundWorker.DoWork"/> event of the background
        /// worker.
        /// </summary>
        /// <param name="sender">The background worker that requested the work.</param>
        /// <param name="e">The data required to do the work.</param>
        private void DownloadAsyncCore(object sender, DoWorkEventArgs e)
        {
            object[] args = e.Argument as object[];
            BackgroundWorker worker = sender as BackgroundWorker;

            if (args != null)
            {
                byte[] buffer = null;
                Uri url = args[0] as Uri;
                string destinationFolder = args[1] as string;

                using (DownloadInfo downloadInfo = new DownloadInfo())
                {
                    downloadInfo.Proxy = this.Proxy;
                    downloadInfo.Credentials = this.Credentials;
                    downloadInfo.PreAuthenticate = this.PreAuthenticate;
                    downloadInfo.DownloadHtml = this.DownloadHtml;
                    downloadInfo.UserAgent = this.userAgent;

                    downloadInfo.Initialize(url, destinationFolder);

                    // Find out the name of the file that the web server gave us.
                    string destFileName = Path.GetFileName(downloadInfo.Response.ResponseUri.ToString());
                    this.downloadingTo = Path.GetFullPath(Path.Combine(destinationFolder, destFileName));

                    if (this.overwrite)
                    {
                        // This is safe because no exceptions will be thrown
                        // if the file doesn't exist.
                        File.Delete(this.downloadingTo);
                    }

                    // Create the file on disk here, so even if we don't receive any data of the file
                    // it's still on disk. This allows us to download 0-byte files.
                    if (!File.Exists(this.downloadingTo))
                    {
                        FileStream fs = File.Create(this.downloadingTo);
                        fs.Close();
                    }

                    // create the download buffer
                    buffer = new byte[this.blockSize];

                    int readCount;

                    // update how many bytes have already been read
                    long totalDownloaded = downloadInfo.StartPoint;

                    while ((int)(readCount = downloadInfo.DownloadStream.Read(buffer, 0, this.blockSize)) > 0)
                    {
                        // break on cancel
                        if (worker.CancellationPending)
                        {
                            e.Cancel = true;
                            downloadInfo.Close();

                            break;
                        }

                        // update total bytes read
                        totalDownloaded += readCount;

                        // save block to end of file
                        SaveToFile(buffer, readCount, this.downloadingTo);

                        // send progress info
                        if (downloadInfo.IsProgressKnown)
                        {
                            int progress = (int)((((double)totalDownloaded) / downloadInfo.Length) * 100);
                            worker.ReportProgress(progress, new long[] { totalDownloaded, downloadInfo.Length });
                        }
                    }
                }
                e.Result = downloadingTo;
            }
        }
        #endregion

        #region DownloadAsyncProgressChanged
        /// <summary>
        /// Raises the <see cref="DownloadProgressChanged"/> event by 
        /// responding to the <see cref="BackgroundWorker.ProgressChanged"/> 
        /// event of the background worker.
        /// </summary>
        /// <param name="sender">The background worker whose progress changed.</param>
        /// <param name="e">The progress change data.</param>
        private void DownloadAsyncProgressChanged(object sender, ProgressChangedEventArgs e)
        {
            long[] args = e.UserState as long[];

            if (args != null)
            {
                this.OnDownloadProgressChanged(args[0], args[1]);
            }
            else
            {
                // If the UserState isn't an array of longs, then it should be a status message.
                string message = e.UserState as string;
                this.OnDownloadStatusChanged(message);
            }
        }
        #endregion

        #region OnDownloadCompleted
        /// <summary>
        /// Raises the <see cref="DownloadCompleted"/> event.
        /// </summary>
        /// <param name="e">The completion data.</param>
        private void OnDownloadCompleted(RunWorkerCompletedEventArgs e)
        {
            if (this.CanRaiseEvents)
            {
                if (this.DownloadCompleted != null)
                {
                    FileDownloadCompletedEventArgs downloadCompleteEventArgs;

                    if (e.Cancelled)
                    {
                        downloadCompleteEventArgs = new FileDownloadCompletedEventArgs("", null, true);
                    }
                    else if (e.Error == null)
                    {
                        downloadCompleteEventArgs = new FileDownloadCompletedEventArgs(e.Result.ToString(), e.Error, e.Cancelled);
                    }
                    else
                    {
                        downloadCompleteEventArgs = new FileDownloadCompletedEventArgs(String.Empty, e.Error, e.Cancelled);
                    }
                    this.DownloadCompleted(this, downloadCompleteEventArgs);
                }
            }
        }
        #endregion

        #region OnDownloadProgressChanged
        /// <summary>
        /// Raises the <see cref="DownloadProgressChanged"/> event.
        /// </summary>
        /// <param name="totalDownloaded">The total number of bytes downloaded.</param>
        /// <param name="totalBytesToReceive">The total number of bytes to be downloaded.</param>
        private void OnDownloadProgressChanged(long totalDownloaded, long totalBytesToReceive)
        {
            if (this.CanRaiseEvents)
            {
                if (this.DownloadProgressChanged != null)
                {
                    FileDownloadProgressChangedEventArgs eventArgs = new FileDownloadProgressChangedEventArgs(totalDownloaded, totalBytesToReceive);
                    this.DownloadProgressChanged(this, eventArgs);
                }
            }
        }
        #endregion

        #region OnDownloadStatusChanged
        /// <summary>
        /// Raises the <see cref="DownloadStatusChanged"/> event.
        /// </summary>
        /// <param name="message">The status message.</param>
        private void OnDownloadStatusChanged(string message)
        {
            if (this.CanRaiseEvents)
            {
                if (this.DownloadStatusChanged != null)
                {
                    FileDownloadStatusChangedEventArgs eventArgs = new FileDownloadStatusChangedEventArgs(message);
                    this.DownloadStatusChanged(this, eventArgs);
                }
            }
        }
        #endregion

        #region SaveToFile
        /// <summary>
        /// Saves the block of bytes to the specified file.
        /// </summary>
        /// <param name="buffer">The <see cref="Byte"/> array to save.</param>
        /// <param name="count">The maximum number of bytes to be written.</param>
        /// <param name="fileName">The file to receive the block of bytes.</param>
        private static void SaveToFile(byte[] buffer, int count, string fileName)
        {
            using (FileStream stream = File.Open(fileName, FileMode.Append, FileAccess.Write))
            {
                stream.Write(buffer, 0, count);
            }
        }
        #endregion

        #endregion


        #region properties

        #region BlockSize
        /// <summary>
        /// Gets or sets the download block size in Kilobytes (KB).
        /// </summary>
        /// <value>The block size used by the download buffer.</value>
        /// <remarks>The value cannot be set to less than the default block size of 1024.</remarks>
        public int BlockSize
        {
            get
            {
                this.CheckDisposed();
                return this.blockSize;
            }
            set
            {
                this.CheckDisposed();
                if (value < FileDownloader.DefaultBlockSize)
                {
                    value = FileDownloader.DefaultBlockSize;
                }
                this.blockSize = value;
            }
        }
        #endregion

        #region Credentials
        /// <summary>
        /// Gets or sets the network credentials used for authenticating the the request with the Internet resource.
        /// </summary>
        /// <value>The <see cref="ICredentials"/> containing the authentication credentials associated with the request. 
        /// The default value is a <see langword="null"/>.</value>
        /// <remarks>The <see cref="Credentials"/> property contains the authentication credentials required to access
        /// the Internet resource.
        /// </remarks>
        public ICredentials Credentials
        {
            get
            {
                this.CheckDisposed();
                return this.credentials;
            }
            set
            {
                this.CheckDisposed();
                this.credentials = value;
            }
        }
        #endregion

        #region DownloadHtml
        /// <summary>
        /// Gets or sets a value indicating if HTML pages should be allowed
        /// to be downloaded.
        /// </summary>
        /// <value><see langowrd="true"/> to download HTML pages; otherwise,
        /// <see langword="false"/>.</value>
        public bool DownloadHtml
        {
            get
            {
                this.CheckDisposed();
                return this.downloadHtml;
            }
            set
            {
                this.CheckDisposed();
                this.downloadHtml = value;
            }
        }
        #endregion

        #region DownloadingTo
        /// <summary>
        /// Gets the name of the destination file.
        /// </summary>
        /// <value>A <see cref="String"/> containing the destination file name.</value>
        /// <remarks>The <see cref="DownloadingTo"/> property will be <see langword="null"/> until a download
        /// request has successfully contacted the server and has begun downloading the file.</remarks>
        public string DownloadingTo
        {
            get
            {
                this.CheckDisposed();
                return this.downloadingTo;
            }
        }
        #endregion

        #region IndefiniteTimeout
        /// <summary>
        /// Gets a value that represents an indefinate timeout for a 
        /// synchronous download operation.
        /// </summary>
        /// <value>A <see cref="TimeSpan"/> that represents an indefinate
        /// timeout.</value>
        public static TimeSpan IndefiniteTimeout
        {
            get
            {
                return new TimeSpan(0, 0, 0, 0, -1);
            }
        }
        #endregion

        #region IsBusy
        /// <summary>
        /// Gets a value indicating whether the <see cref="FileDownloader"/> is
        /// running an asynchronous operation.
        /// </summary>
        /// <value><see langword="true"/> if an asynchronous operation is
        /// running; otherwise, <see langword="false"/>.</value>
        public bool IsBusy
        {
            get
            {
                this.CheckDisposed();
                return this.backgroundWorker.IsBusy;
            }
        }
        #endregion

        #region Overwrite
        /// <summary>
        /// Gets or sets a value indicating if the existing file should be
        /// overwritten.
        /// </summary>
        /// <value><see langword="true"/> if the file should be overwritten;
        /// otherwise, <see langword="false"/>.</value>
        public bool Overwrite
        {
            get
            {
                this.CheckDisposed();
                return this.overwrite;
            }
            set
            {
                this.CheckDisposed();
                this.overwrite = value;
            }
        }
        #endregion

        #region PreAuthenticate
        /// <summary>
        /// Indicates whether to pre-authenticate the request.
        /// </summary>
        /// <value><see langword="true"/> to pre-authenticate; otherwise,
        /// <see langword="false"/>.</value>
        /// <remarks>With the exception of the first request, the 
        /// <see cref="PreAuthenticate"/> property indicates whether to send
        /// authentication information with subsequent requests without waiting
        /// to be challenged by the server. When <see cref="PreAuthenticate"/>
        /// is <see langword="false"/>, the <see cref="WebRequest"/> waits
        /// for an authentication challenge before sending authentication
        /// information.</remarks>
        public bool PreAuthenticate
        {
            get
            {
                this.CheckDisposed();
                return this.preAuthenticate;
            }
            set
            {
                this.CheckDisposed();
                this.preAuthenticate = value;
            }
        }
        #endregion

        #region Proxy
        /// <summary>
        /// Gets or sets proxy information for the request.
        /// </summary>
        /// <value>The <see cref="IWebProxy"/> object to use to proxy the request. The default value is set to the <see cref="WebRequest.DefaultWebProxy"/> property. </value>
        /// <remarks>The <see cref="Proxy"/> property identifies the WebProxy object to use to process requests to Internet
        /// resources. To specify that no proxy should be used, set the Proxy property to the proxy instance returned by 
        /// the <see cref="GlobalProxySelection.GetEmptyWebProxy"/> method.
        /// </remarks>
        public IWebProxy Proxy
        {
            get
            {
                this.CheckDisposed();
                return this.proxy;
            }
            set
            {
                this.CheckDisposed();
                this.proxy = value;
            }
        }
        #endregion

        #region Timeout
        /// <summary>
        /// Gets or sets a value indicating the timeout for asynchronous
        /// download operations.
        /// </summary>
        /// <value>A <see cref="TimeSpan"/> representing the timeout.</value>
        public TimeSpan Timeout
        {
            get
            {
                this.CheckDisposed();
                return this.timeout;
            }
            set
            {
                this.CheckDisposed();
                this.timeout = value;
            }
        }
        #endregion

        #region UserAgent
        /// <summary>
        /// Gets or sets the value of the <b>User-agent</b> HTTP header. 
        /// </summary>
        /// <value>The value of the <b>User-agent</b> HTTP header. The default
        /// value is <see langword="null"/>.</value>
        public string UserAgent
        {
            get
            {
                this.CheckDisposed();
                return this.userAgent;
            }
            set
            {
                this.CheckDisposed();
                this.userAgent = value;
            }
        }
        #endregion

        #region Methods

        #region Dispose
        /// <summary>
        /// Releases the resources, other than memory, that are used by the <see cref="FileDownloader"/>. 
        /// </summary>
        /// <param name="disposing"><b>true</b> to release both managed and unmanaged resources; <b>false</b> to release only unmanaged resources.</param>
        protected override void Dispose(bool disposing)
        {
            try
            {
                if (!disposed)
                {
                    if (disposing)
                    {
                        // Dispose managed resources.
                        if (this.backgroundWorker != null)
                        {
                            this.backgroundWorker.DoWork -= this.DownloadAsyncCore;
                            this.backgroundWorker.RunWorkerCompleted -= this.DownloadAsyncCompleted;
                            this.backgroundWorker.ProgressChanged -= this.DownloadAsyncProgressChanged;
                            this.backgroundWorker.Dispose();
                        }

                        if (this.syncEvent != null)
                        {
                            this.syncEvent.Close();
                        }
                    }
                }
            }
            finally
            {
                base.Dispose(disposing);
                disposed = true;
            }
        }
        #endregion

        #region Download

        #region Download(string url)
        /// <summary>
        /// Download the file at the specified url, and save it to the current folder.
        /// </summary>
        /// <param name="url">The <see cref="Uri"/> of the file to be downloaded.</param>
        public void Download(string url)
        {
            if (String.IsNullOrEmpty(url))
            {
                throw new ArgumentNullException("url");
            }

            Download(new Uri(url), String.Empty);
        }
        #endregion

        #region Download(string url, string destinationFolder)
        /// <summary>
        /// Download the file at the specified url, and save it to the specified folder.
        /// </summary>
        /// <param name="url">The <see cref="Uri"/> of the file to be downloaded.</param>
        /// <param name="destinationFolder">The output destination for the file.</param>
        public void Download(string url, string destinationFolder)
        {
            if (String.IsNullOrEmpty(url))
            {
                throw new ArgumentNullException("url");
            }

            if (destinationFolder == null)
            {
                throw new ArgumentNullException("destinationFolder");
            }

            Download(new Uri(url), destinationFolder);
        }
        #endregion

        #region Download(Uri url)
        /// <summary>
        /// Download the file at the specified url, and save it to the current folder.
        /// </summary>
        /// <param name="url">The <see cref="Uri"/> of the file to be downloaded.</param>
        public void Download(Uri url)
        {

            Download(url, String.Empty);
        }
        #endregion

        #region Download(Uri url, string destinationFolder)
        /// <summary>
        /// Download the file at the specified url, and save it to the specified folder.
        /// </summary>
        /// <param name="url">The <see cref="Uri"/> of the file to be downloaded.</param>
        /// <param name="destinationFolder">The output destination for the file.</param>
        public void Download(Uri url, string destinationFolder)
        {
            if (destinationFolder == null)
            {
                throw new ArgumentNullException("destinationFolder");
            }

            this.CheckDisposed();

            this.backgroundWorker.RunWorkerAsync(new object[] { url, destinationFolder });
            this.syncEvent.WaitOne(this.timeout, false);
        }
        #endregion

        #endregion

        #region DownloadAsync

        #region DownloadAsync(string url)
        /// <summary>
        /// Begin downloading the file at the specified url, and save it to the current folder.
        /// </summary>
        /// <param name="url">The <see cref="Uri"/> of the file to be downloaded.</param>
        public void DownloadAsync(string url)
        {
            if (String.IsNullOrEmpty(url))
            {
                throw new ArgumentNullException("url");
            }

            DownloadAsync(new Uri(url), String.Empty);
        }
        #endregion

        #region DownloadAsync(string url, string destinationFolder)
        /// <summary>
        /// Begin downloading the file at the specified url, and save it to the specified folder.
        /// </summary>
        /// <param name="url">The <see cref="Uri"/> of the file to be downloaded.</param>
        /// <param name="destinationFolder">The output destination for the file.</param>
        public void DownloadAsync(string url, string destinationFolder)
        {
            if (String.IsNullOrEmpty(url))
            {
                throw new ArgumentNullException("url");
            }

            if (destinationFolder == null)
            {
                throw new ArgumentNullException("destinationFolder");
            }

            DownloadAsync(new Uri(url), destinationFolder);
        }
        #endregion

        #region DownloadAsync(Uri url)
        /// <summary>
        /// Begin downloading the file at the specified url, and save it to the current folder.
        /// </summary>
        /// <param name="url">The <see cref="Uri"/> of the file to be downloaded.</param>
        public void DownloadAsync(Uri url)
        {
            DownloadAsync(url, String.Empty);
        }
        #endregion

        #region DownloadAsync(Uri url, string destinationFolder)
        /// <summary>
        /// Begin downloading the file at the specified url, and save it to the specified folder.
        /// </summary>
        /// <param name="url">The <see cref="Uri"/> of the file to be downloaded.</param>
        /// <param name="destinationFolder">The output destination for the file.</param>
        public void DownloadAsync(Uri url, string destinationFolder)
        {
            if (destinationFolder == null)
            {
                throw new ArgumentNullException("destinationFolder");
            }

            this.CheckDisposed();

            this.backgroundWorker.RunWorkerAsync(new object[] { url, destinationFolder });
        }
        #endregion

        #endregion

        #region DownloadAsyncCancel
        /// <summary>
        /// Cancel the current download operation.
        /// </summary>
        public void DownloadAsyncCancel()
        {
            this.CheckDisposed();

            this.backgroundWorker.CancelAsync();
        }
        #endregion

        #region GetLastModifiedDate

        #region GetLastModifiedDate(string url)
        /// <summary>
        /// Retrieves the last modified date of the file at the specified url.
        /// </summary>
        /// <param name="url">The <see cref="Uri"/> of the file.</param>
        /// <returns>The <see cref="DateTime"/> the file was last modified on
        /// the server, in Universal Time. If the last modified date is not 
        /// able to be determined, a <see langword="null"/> is returned.</returns>
        public DateTime? GetLastModifiedDate(string url)
        {
            if (String.IsNullOrEmpty(url))
            {
                throw new ArgumentNullException("url");
            }

            return GetLastModifiedDate(new Uri(url));
        }
        #endregion

        #region GetLastModifiedDate(Uri url)
        /// <summary>
        /// Retrieves the last modified date of the file at the specified url.
        /// </summary>
        /// <param name="url">The <see cref="Uri"/> of the file.</param>
        /// <returns>The <see cref="DateTime"/> the file was last modified on
        /// the server, in Universal Time. If the last modified date is not 
        /// able to be determined, a <see langword="null"/> is returned.</returns>
        public DateTime? GetLastModifiedDate(Uri url)
        {
            this.CheckDisposed();

            using (DownloadInfo downloadInfo = new DownloadInfo())
            {
                downloadInfo.Proxy = this.Proxy;
                downloadInfo.Credentials = this.Credentials;
                downloadInfo.PreAuthenticate = this.PreAuthenticate;
                downloadInfo.DownloadHtml = this.DownloadHtml;

                return downloadInfo.GetLastModifiedDate(url);
            }
        }
        #endregion

        #endregion

        #endregion

        #endregion

    }
}
